package com.avcompris.util;

import static com.avcompris.util.ExceptionUtils.nonNullArgument;
import static org.apache.commons.io.FileUtils.openInputStream;
import static org.apache.commons.lang3.StringUtils.substringAfter;
import static org.apache.commons.lang3.StringUtils.substringBetween;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import com.avcompris.common.annotation.Nullable;
import com.google.common.collect.Iterators;

/**
 * XPath utilities.
 * 
 * @author David Andrianavalontsalama Copyright 2008-2011 ©
 */
public abstract class XPathUtils extends AbstractUtils {

	/**
	 * the loaded {@link XPathFactory}
	 */
	private static XPathFactory xpathFactory = null;

	/**
	 * load the {@link XPathFactory} if it has not yet been loaded.
	 */
	private static synchronized XPathFactory loadXPathFactory() {

		if (xpathFactory == null) {

			xpathFactory = XPathFactory.newInstance();
		}

		return xpathFactory;
	}

	/**
	 * extract an unique XPath value from a XML file.
	 * 
	 * @param file the XML file
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated XPath value
	 */
	public static String eval(final File file, final String xpath,
			@Nullable final String... ns) throws IOException, XPathExpressionException {

		nonNullArgument(file, "file");
		nonNullArgument(xpath, "xpath");
		nonNullArgument(ns, "ns");

		final InputStream is = openInputStream(file);
		try {

			final InputSource inputSource = new InputSource(is);

			return eval(inputSource, xpath, ns);

		} finally {
			is.close();
		}
	}

	/**
	 * extract a list of XPath values from a XML file.
	 * 
	 * @param file the XML file
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated XPath values
	 */
	public static String[] arrayEval(final File file, final String xpath,
			@Nullable final String... ns) throws IOException, XPathExpressionException {

		nonNullArgument(file, "file");
		nonNullArgument(xpath, "xpath");
		nonNullArgument(ns, "ns");

		final InputStream is = openInputStream(file);
		try {

			final InputSource inputSource = new InputSource(is);

			return arrayEval(inputSource, xpath, ns);

		} finally {
			is.close();
		}
	}

	/**
	 * extract an unique integer XPath value from a XML file.
	 * 
	 * @param file the XML file
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated integer XPath value
	 */
	public static int intEval(final File file, final String xpath,
			@Nullable final String... ns) throws IOException, XPathExpressionException {

		final String eval = eval(file, xpath, ns);

		return parseInt(eval, xpath);
	}

	/**
	 * extract an unique boolean XPath value from a XML file.
	 * 
	 * @param file the XML file
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated boolean XPath value
	 */
	public static boolean booleanEval(final File file, final String xpath,
			@Nullable final String... ns) throws IOException, XPathExpressionException {

		final String eval = eval(file, xpath, ns);

		return parseBoolean(eval, xpath);
	}

	/**
	 * parse a String value into a integer for a given XPath expression.
	 * 
	 * @param eval the initial String value
	 * @param xpath the XPath expression that has been evaluated
	 */
	private static int parseInt(final String eval, @Nullable final String xpath) {

		nonNullArgument(eval, "eval");

		final int intEval;

		try {

			intEval = Integer.parseInt(eval);

		} catch (final NumberFormatException e) {

			throw new RuntimeException(
					"Cannot parse integer value for XPath \"" + xpath + "\": "
							+ eval, e);
		}

		return intEval;
	}

	/**
	 * parse a String value into a boolean for a given XPath expression.
	 * 
	 * @param eval the initial String value
	 * @param xpath the XPath expression that has been evaluated
	 */
	private static boolean parseBoolean(final String eval,
			@Nullable final String xpath) {

		nonNullArgument(eval, "eval");

		final boolean booleanEval;

		try {

			booleanEval = Boolean.parseBoolean(eval);

		} catch (final NumberFormatException e) {

			throw new RuntimeException(
					"Cannot parse boolean value for XPath \"" + xpath + "\": "
							+ eval, e);
		}

		return booleanEval;
	}

	/**
	 * extract an unique XPath value from a XML content.
	 * 
	 * @param xml the XML content
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated XPath value
	 */
	public static String eval(final String xml, final String xpath,
			@Nullable final String... ns) throws IOException, XPathExpressionException {

		nonNullArgument(xml, "xml");
		nonNullArgument(xpath, "xpath");

		final Reader reader = new StringReader(xml);
		try {

			final InputSource inputSource = new InputSource(reader);

			return eval(inputSource, xpath, ns);

		} finally {
			reader.close();
		}
	}

	/**
	 * extract an unique integer XPath value from a XML content.
	 * 
	 * @param xml the XML content
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated integer XPath value
	 */
	public static int intEval(final String xml, final String xpath,
			@Nullable final String... ns) throws IOException, XPathExpressionException {

		final String eval = eval(xml, xpath, ns);

		return parseInt(eval, xpath);
	}

	/**
	 * extract an unique boolean XPath value from a XML content.
	 * 
	 * @param xml the XML content
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated boolean XPath value
	 */
	public static boolean booleanEval(final String xml, final String xpath,
			@Nullable final String... ns) throws IOException, XPathExpressionException {

		final String eval = eval(xml, xpath, ns);

		return parseBoolean(eval, xpath);
	}

	/**
	 * extract an unique XPath value from a XML content.
	 * 
	 * @param inputSource the XML input source
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated XPath value
	 */
	public static String eval(final InputSource inputSource,
			final String xpath, @Nullable final String... ns) throws XPathExpressionException {

		nonNullArgument(inputSource, "inputSource");
		nonNullArgument(xpath, "xpath");

		final XPath x = loadXPathFactory().newXPath();

		x.setNamespaceContext(calcNamespaceContext(ns));

		final String eval = x.evaluate(xpath, inputSource);

		return eval;
	}

	/**
	 * create a {@link NamespaceContext} that resolves.
	 * 
	 * @param ns the namespaces as key/value pairs, or as expressions. They
	 * may be mixed. Examples:
	 * <ul>
	 * <li><tt>"pom", "http://maven.apache.org/POM/4.0.0"</tt>
	 * <li><tt>"xmlns:pom=http://maven.apache.org/POM/4.0.0"</tt>
	 * <li><tt>"pom", "http://maven.apache.org/POM/4.0.0", "xsi", "http://www.w3.org/2001/XMLSchema-instance"</tt>
	 * <li><tt>"xmlns:pom=http://maven.apache.org/POM/4.0.0", "xsi", "http://www.w3.org/2001/XMLSchema-instance"</tt>
	 * </ul>
	 */
	public static NamespaceContext calcNamespaceContext(final String... ns) {

		nonNullArgument(ns, "ns");

		final Map<String, String> uris = calcNamespaceMap(ns);

		return createNamespaceContext(uris);
	}

	/**
	 * create a {@link NamespaceContext} that resolves,
	 * based on a collection of "<tt>prefix/URI</tt>" associations.
	 */
	public static NamespaceContext createNamespaceContext(
			final Map<String, String> uris) {

		nonNullArgument(uris, "uris");

		return new NamespaceContext() {

			@Override
			public String toString() {

				return uris.toString();
			}

			@Override
			public String getNamespaceURI(final String prefix) {

				nonNullArgument(prefix, "prefix");

				if (XMLConstants.DEFAULT_NS_PREFIX.equals(prefix)) {
					return XMLConstants.NULL_NS_URI;
				} else if (XMLConstants.XML_NS_PREFIX.equals(prefix)) {
					return XMLConstants.XML_NS_URI;
				} else if (XMLConstants.XMLNS_ATTRIBUTE.equals(prefix)) {
					return XMLConstants.XMLNS_ATTRIBUTE_NS_URI;
				} else {

					final String uri = uris.get(prefix);

					if (uri != null) {

						return uri;
					}

					return XMLConstants.NULL_NS_URI;
				}
			}

			@Override
			public String getPrefix(final String namespaceURI) {

				nonNullArgument(namespaceURI, "namespaceURI");

				final String def = getDefaultNamespaceUriPrefix(namespaceURI);

				if (def != null) {

					return def;
				}

				for (final Map.Entry<String, String> entry : uris.entrySet()) {

					if (namespaceURI.equals(entry.getValue())) {

						return entry.getKey();
					}
				}

				return null;
			}

			@Override
			public Iterator<?> getPrefixes(final String namespaceURI) {

				final String def = getDefaultNamespaceUriPrefix(namespaceURI);

				if (def != null) {

					return Iterators.singletonIterator(def);
				}

				final Set<String> prefixes = new HashSet<String>();

				for (final Map.Entry<String, String> entry : uris.entrySet()) {

					if (namespaceURI.equals(entry.getValue())) {

						prefixes.add(entry.getKey());
					}
				}

				return prefixes.iterator();
			}
		};
	}

	private static String getDefaultNamespaceUriPrefix(
			@Nullable final String namespaceUri) {

		if (XMLConstants.NULL_NS_URI.equals(namespaceUri)) {
			return XMLConstants.DEFAULT_NS_PREFIX;
		} else if (XMLConstants.XML_NS_URI.equals(namespaceUri)) {
			return XMLConstants.XML_NS_PREFIX;
		} else if (XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(namespaceUri)) {
			return XMLConstants.XMLNS_ATTRIBUTE;
		} else {
			return null;
		}
	}

	public static Map<String, String> calcNamespaceMap(final String... ns) {

		nonNullArgument(ns, "ns");

		final Map<String, String> uris = new HashMap<String, String>();

		for (int i = 0; i < ns.length; ++i) {

			final String s = ns[i];

			if (s.startsWith("xmlns:") && s.contains("=")) {

				final String prefix = substringBetween(s, "xmlns:", "=");

				String uri = substringAfter(s, "=");

				if (uri.startsWith("'")) {

					uri = substringBetween(uri, "'");

				} else if (uri.startsWith("\"")) {

					uri = substringBetween(uri, "\"");
				}

				uris.put(prefix, uri);

			} else {

				final String prefix = s;

				++i;

				if (i < ns.length) {

					final String uri = ns[i];

					uris.put(prefix, uri);
				}
			}
		}

		return uris;
	}

	/**
	 * extract a list of XPath values from a XML content.
	 * 
	 * @param inputSource the XML input source
	 * @param xpath the XPath expression
	 * @param ns the [prefix, namespaceURI] pairs for namespace resolution
	 * @return the evaluated XPath values
	 */
	public static String[] arrayEval(final InputSource inputSource,
			final String xpath, @Nullable final String... ns) throws XPathExpressionException {

		nonNullArgument(inputSource, "inputSource");
		nonNullArgument(xpath, "xpath");

		final XPath x = loadXPathFactory().newXPath();

		x.setNamespaceContext(calcNamespaceContext(ns));

		final NodeList nodeList = (NodeList) x.evaluate(xpath, inputSource,
				XPathConstants.NODESET);

		final int count = nodeList.getLength();

		final String[] eval = new String[count];

		for (int i = 0; i < count; ++i) {

			eval[i] = nodeList.item(i).getFirstChild().getNodeValue();
		}

		return eval;
	}
}
